package com.ddf.springdata.jpa.dao;

import com.ddf.springdata.jpa.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.stream.Stream;

/**
 * @author DDf on 2018/9/27
 * 声明该接口即可，SpringData会自动继承的接口中的方法进行实现，对于使用者来说，直接拿来使用即可。除了接口中声明的方法可以使用外，
 * 还可以按照一定的规则自定义查询接口
 * 如果有自定义的方法，可以使用该接口同时去继承自定义的接口
 * <p>
 * 几个比较重要的使用说明：
 * 1. 方便的继承SpringMVC可以通过参数，直接自己查询接收参数的对象，可以方便的直接用Pageable来接收分页参数
 * https://docs.spring.io/spring-data/jpa/docs/2.0.10.RELEASE/reference/html/#core.web
 * <p>
 * 2. 自定义Base Repository
 * https://docs.spring.io/spring-data/jpa/docs/2.0.10.RELEASE/reference/html/#repositories.customize-base-repository
 * <p>
 * 3. 查询表达式的使用
 * https://docs.spring.io/spring-data/jpa/docs/2.0.10.RELEASE/reference/html/#jpa.query-methods.query-creation
 * 3.1 查询方法的结果可以通过使用first或top关键字来限制，这些关键字可以互换使用。可以附加一个可选的数值，
 * top或者first指定要返回的最大结果大小。如果省略该数字，则假定结果大小为1。以下示例显示如何限制查询大小：
 * 3.2 查询表达式的各个部分使用有严格的控制，但是如果定义过多，可能会重复，所以只能在get/findUserxxxBy，User和By之间加一些修饰来区分
 * <p>
 * 4. 运用@Query,默认是通过对象和属性来操作数据库，如果使nativesql的话，请使用属性nativeQuery = true
 * https://docs.spring.io/spring-data/jpa/docs/2.0.10.RELEASE/reference/html/#jpa.query-methods.at-query
 * 5. 如果实体类与其他实体类有映射，则支持使用其它类属性来查询，则查询表达式也适用，不过在使用其他类属性之前要先在属性前加对象名，如果使用多个属性，
 *      则每个属性前都要加
 * 6. 查询表达式可以Projections功能返回部分字段，而且支持关联表字段，参考{@link NamePasswordOnly}，{@link findUserById}可重用findUserById
 * 7. 关于事务@Transactional(readOnly = true)，如果是只读的事务，建议加上这个注解，框架会有优化。另外有些方法在Repository接口中直接定义就可以
 *    使用，因此事务加在这里是可以接受的，但是Service层可能会同时调用多个Dao,因此Service层加上注解应该是最合适的。或者为保证某些dao被直接调用，
 *    dao加上事务也可以，毕竟事务如果不特殊处理的话，同个service一旦事务开启，调用多个dao方法也是同一个事务,所以建议可以把@Transactional(readOnly = true)
 *    加在Repository类上作为默认的事务，而一旦牵扯到其它更新，service层自己会先行开启可以修改并提交的事务
 * 8. Autid，我们提供@CreatedBy和@LastModifiedBy捕捉谁创建或修改的实体以及用户@CreatedDate和@LastModifiedDate捕捉时的变化发生了。
 *      @CreatedDate和@LastModifiedDate的使用步骤
 *      8.1 配置类上开启功能@EnableJpaAuditing
 *      8.2 需要使用的对象上@EntityListeners(AuditingEntityListener.class)，依赖spring-aspects
 *      8.3 需要使用的字段上@CreatedDate或@LastModifiedDate
 *      8.4 如果需要使用@CreatedBy和@LastModifiedBy，则需要实现AuditorAware<T>SPI接口，
 *      如果你使用@CreatedBy或者@LastModifiedBy，审计基础设施需要知道当前的主体。为此，我们提供了一个AuditorAware<T>SPI接口，您必须实现该接口，
 *      以告知基础架构当前用户或系统与应用程序交互的人员。泛型类型T定义了带注释@CreatedBy或@LastModifiedBy必须注释的属性的类型。
 *      https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/#auditing.basics
 */
@Repository
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Integer>, UserDao, JpaBaseDao {
    /**
     * 根据username和password全部相等查询
     *
     * @param username
     * @param password
     * @return
     */
    User getByUsernameAndPassword(String username, String password);

    /**
     * 根据username和password LIKE 模糊查询，且两个条件的关系为OR
     *
     * @param username
     * @param password
     * @return
     */
    List<User> getByUsernameLikeOrPasswordLike(String username, String password);

    /**
     * 经过测试发现：
     * 只能用在有事务的service方法中，并且超出该Service方法后将Stream返回给其它调用者之后，并不能操作Stream,
     * 否则会抛出异常stream has already been operated upon or closed
     */
    Stream<User> getAllByCreateTimeBefore(Date date);

    /**
     * 根据名称模糊匹配分页查询,可以包含排序
     *
     * @param username
     * @param pageable
     * @return
     */
    Page<User> getAllByUsernameLike(String username, Pageable pageable);

    Slice<User> findByUsernameLike(String username, Pageable pageable);

    @Query(value = "from User where username like :username")
    List<User> listByUserNameLikeStart(@Param("username") String username);

    /**
     * 必须使用@Modifying注解来标识这里是更新而不是查询
     * @param oldName
     * @param password
     */
    @Transactional
    @Modifying
    @Query(value = "update User u set u.password = :password where u.username = :username")
    void updateUserByUsername(@Param("username") String oldName, @Param("password") String password);

    /**
     * 没看懂啥意思，用起来也没发现什么不一样的地方
     * https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/#jpa.entity-graph
     * @param username
     * @return
     */
    @EntityGraph(value = "User.detail", type = EntityGraph.EntityGraphType.LOAD)
    User getByUsername(String username);

    /**
     * 自定义只查询封装的特定字段
     * @see NamePasswordOnly
     * @return
     */
    List<NamePasswordOnly> getAllBy();

    /**
     * 在数据库中引用名为“plus1inout”的显式映射过程。
     * @Procedure 指定存储过程名称
     *
     DROP procedure IF EXISTS plus1inout

     CREATE procedure plus1inout (IN arg int, OUT res int)
     BEGIN
     set res = arg + 1;
     END
     * @param arg
     * @return
     */
    @Procedure("plus1inout")
    Integer explicitlyNamedPlus1inout(Integer arg);

    /**
     * 如果想要只返回实体类的某些字段，则可以通过将要查询出的字段的get方法暴露出来，称为封闭式预测
     * 建议保持这种写法，这样SpringData会提供优化，其它实现方式参考下面链接
     * https://docs.spring.io/spring-data/jpa/docs/2.1.0.RELEASE/reference/html/#projections
     */
    interface NamePasswordOnly {
        String getUsername();
        String getPassword();
        /** getIdCard对象的CardNo */
        CardNoOnly getIdCardCardNo();
        interface CardNoOnly {
            String getIdCardCardNo();
        }
        default String getSummary() {
            return "username: " + getUsername() + ", password: " + getPassword() + ", cardNo: " + getIdCardCardNo();
        }
    }
}


